iT邦幫忙

2024 iThome 鐵人賽

DAY 27
1
JavaScript

Don't make JavaScript Just Surpise系列 第 27

模組(Module)的前世今生 - ES 6 Module

  • 分享至 

  • xImage
  •  

繼上篇講過的模組標準百花齊放後,迎來了分久必合,到了 ES 6,ECMAScript 終於有了關於模組的標準定義,寫於 Module 章節裡。

當模組指稱 ES 6 的 Module 的時候,我們可能會簡寫為 ESM(ES 6 Module)。

在 ESM 裡面,引入和導出分別使用 importexport 兩個關鍵字。
ESM 把單一的檔案視為一個模組,作用域就鎖於該檔案內,因此引入後不會有全域污染的問題。

導出時使用 export,對象可以是物件、函示、類別、變數。
另外導出時可以決定要匿名預設導出或具名導出。

// module.js
export function namedExport() {//具名導出
    console.log('I am a named export');
}

export default function() {//匿名預設導出
    console.log('I am the default export');
}

可以看到範例多了一個關鍵字 default,該關鍵字能起到的作用是,透過 export default 導出的內容,引入時無需使用大括號命名去接,而是會直接接到該 default 對象。
同時,整個模組內只能有一個 default,多於一個會直接報錯。(Identifier '.default' has already been declared")。

引入時使用 import,加上大括號來對應導出對象的名字,除非是使用 export default 方式導出的;引入時模組來源可用相對路徑,但要包含完整檔名如 .js(某些環境透過設定可能可以省略)。
以上面導出的例子而言,假設 module.js 是一個同路徑下的檔案,可以這樣寫:

import defaultExport, { namedExport } from './module.js';
// 使用 foo 和 bar 函數
namedExport(); // "I am a named export"
defaultExport(); // "I am the default export"

這個例子也示範了具名和預設匿名導出是可以混用的。

ESM 引入或導出的的時候,有一個關鍵字 as 可以用於重新命名本來檔案裡的函式或物件,如下面例子,本來原檔案裡匯出的函式是 function1 和 function2,但 as 讓函式在外面被引入時改為使用 newFunctionName 和 anotherNewFunctionName 來引入。

//module.js
export { function1 as newFunctionName, function2 as anotherNewFunctionName };
//main.js
import {newFunctionName, anotherNewFunctionName} from './module.js';

//as 寫在 import 的寫法
export { function1 , function2};
import { function1 as newFunctionName, function2 as anotherNewFunctionName} from './module.js';

重命名讓我們在引入模組的時候能避免名稱衝突,也可以使用更貼近該檔案風格的命名。

瀏覽器中的 ESM 的模組載入時間是屬於延遲載入,有點類似過往的語法中在 <script> 上加上 defer 的感覺,不會阻塞頁面的渲染,會等到 HTML 解析完成後,在 Load 事件觸發前先載入。

import 的語法屬同步載入,會待載入完才繼續往下執行。
但若要動態載入, ESM 也有提供 import() 的寫法,使撰寫時更為靈活。

import("customModule.js").then((module) => {
  // Do something with the module.
});

這樣的寫法 import() 會返回一個 Promise 物件,可以用對應的 .then() 的寫法去做後續載入模組的處理。

當使用到 exportimport 時有以下幾點需要注意:

  1. exportimport 兩個關鍵字都只能在各個檔案內的頂層作用域中被使用,否則會無法識別而報錯。
  2. 只要該檔案有用到任何一句的 importexport,整個檔案就會自動變為嚴格模式("use strict"),有用到的時候要注意該檔案有沒有用到任何會在嚴格模式出錯的語法。
  3. 在 html 的 script tag 中的語法若有用到 exportimport,載入需有 type="module" 的屬性。預設的屬性是 type="application/javascript"

Webpack & Tree Shaking

撰寫模組和程式的時候,包含整理依賴與轉換成適合執行的方式,有個打包的概念:指經過編譯工具,產出最後實際執行的 JS 檔。
如以往在 CJS 的時候,有 Browserify 來打包 CJS 提供給前端使用。

後來 Webpack 被推出了,作為更新更靈活的 JS 模組打包工具,旨在提供簡化優化模組的管理,包含前後端都能使用。
相較於 Browserify,Webpack 可適用於 CJS,AMD,CMD,UMD,ESM,甚至除了模組外,還能夠打包 CSS 和圖片格式(給前端使用)。

Webpack 有個優化打包的概念: Tree Shaking(中文有人會說樹搖,但使用不廣泛,大部分會直接說英文)。
具象化英文翻譯就是一個搖動樹的概念,把樹上的枯葉抖下來。這個概念被具象化到打包中,即指透過靜態分析的優勢,把沒有用到的程式碼在編譯的時期移除,減少最後文件打包的大小。

副檔名.mjs.js

看別人的程式碼可能會看到有些檔案的副檔名是 .mjs,如 V8引擎 推薦了這種做法。
好處是可以清楚看出哪些是作為模組作用,哪些是一般的 JS,某些運行環境可能也要求這種方式來便是模組。

但在現今的環境中,可能仍有瀏覽器/作業系統/伺服器沒辦法正確識別的這種副檔名。
就伺服器而言,對於各個文件而言會有一個 Content-Type,需要指定文件的 MIME 類型,最好是用 text/javascript 來標示,這是 HTML standard 裡推薦的標示方法。
像 MDN 可能還有提到包含 javascript/esmapplication/javascript,但如上所說,於標準中被推薦的寫法是 type="text/javascript"

對於瀏覽器而言,如果是 <script> 標籤的引入方法需要加註 type="module",否則無法正確被識別 importexport 語法。

<script type="module"> //include script here </script>

小結:比較

講完了 JavaScript 中常見的五種模組標準(CJS、AMD、CMD、UMD、ESM),我們來最後做一個表格比較。

標準 模組載入時間 導出與引入 主要使用環境 使用場景
CJS 同步載入 require / module.exports Node.js 簡單易用,適合後端模組,不適合前端瀏覽器環境,無法異步加載。
AMD 異步載入 define / require 瀏覽器 異步載入,適合前端。
CMD 異步載入 define / require 瀏覽器 按需加載模組,依賴可以延遲執行。
UMD 依據實際應用的方式 支持多種格式 (CJS, AMD) 瀏覽器與 Node.js 通用性強,適合跨平台應用,但程式碼會多一段要處理相容部分。
ESM 提供同步載入和異步載入語法 import / export 現代瀏覽器和 Node.js 語法簡潔,支持靜態分析和 Tree Shaking

在現今的開發環境中,ESM 已是主流的寫法,透過整合的規範,只要是現代的瀏覽器,幾乎都會使用 ESM。當然過往的專案可能仍有使用其他載入方法的,希望這兩篇能幫助大家更全面的了解 JS 中的模組概念。


上一篇
模組(Module)的前世今生 - IIFE、CommonJS、AMD、UMD、CMD
下一篇
垃圾回收機制(Garbage Collection)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言